package com.shallowdream.threaddemo.continueftp;

import com.alibaba.fastjson.JSONArray;
import org.apache.catalina.util.ServerInfo;
import org.apache.commons.beanutils.BeanUtils;
import org.apache.commons.codec.CharEncoding;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.net.PrintCommandListener;
import org.apache.commons.net.ftp.FTP;
import org.apache.commons.net.ftp.FTPClient;
import org.apache.commons.net.ftp.FTPFile;
import org.apache.commons.net.ftp.FTPReply;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.*;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

/**
 * 参数较多通过builder模式构建实体
 * 支持断点续传的FTP实用类
 * 以下方法所有涉及到路径的入参均为”绝对路径“
 *
 * @version 0.1 实现基本断点上传下载
 */
public class FtpTranClient1 {
    Logger logger = LoggerFactory.getLogger(FtpTranClient1.class);
    private final String seprator = "/";
    //Ftp下载源，上传时，此对象置为空即可
    FtpServerInfo ftpServerInfo;
    //上传目标服务器
    List<FtpServerInfo> tarGetFtpServerInfoList;

    public FtpServerInfo getFtpServerInfo() {
        return ftpServerInfo;
    }


    public void pushTarGetFtpClient(FtpServerInfo ftpServerInfo) {
        if (CollectionUtils.isEmpty(tarGetFtpServerInfoList)) {
            tarGetFtpServerInfoList = new ArrayList<>();
        }
        tarGetFtpServerInfoList.add(ftpServerInfo);
    }

    public FtpTranClient1(Builder builder) {
        this.ftpServerInfo = new FtpServerInfo();
        this.ftpServerInfo.setIp(builder.ip);
        this.ftpServerInfo.setPort(builder.port);
        this.ftpServerInfo.setUsername(builder.username);
        this.ftpServerInfo.setPassword(builder.password);
        this.ftpServerInfo.setCharSet(builder.charSet);
        this.ftpServerInfo.setFileType(builder.fileType);
        this.ftpServerInfo.setRemotePath(builder.remotePath);
        FTPClient ftpClient = new FTPClient();
        this.ftpServerInfo.setFtpClient(ftpClient);
    }

    /**
     * 连接到FTP服务器
     *
     * @return 是否连接成功
     * @throws IOException
     */
    public void connect() throws IOException {
        connect(this.ftpServerInfo);
        if (CollectionUtils.isNotEmpty(tarGetFtpServerInfoList)) {
            for (FtpServerInfo serverInfo : tarGetFtpServerInfoList) {
                connect(serverInfo);
            }
        }
    }

    /**
     * 目录对目录
     * 将本地某个文件路径下的所有文件上传至Ftp服务器的某个路径，文件名称一一对应
     *
     * @param localDrectionPath 本地文件夹路径
     * @param recursive         本地文件路径是否递归
     */
    public FileStatus uploadDre(String localDrectionPath, boolean recursive) throws IOException {
        //本地路径校验
        FileStatus fileStatus = localPathJudge(localDrectionPath, recursive);
        if (fileStatus != FileStatus.File_ok) {
            return fileStatus;
        }
        //获取本地所有文件
        List<File> localFileList = (List<File>) FileUtils.listFiles(new File(localDrectionPath), null, recursive);
        logger.info("获取文件列表:{}", JSONArray.toJSONString(localFileList));
        for (File file : localFileList) {
            uploadFile(file, this.ftpServerInfo.getRemotePath() + seprator + file.getName());
        }
        return null;
    }

    /**
     * 文件路径对文件路径
     *
     * @param localFilePath 本地文件路径
     * @return
     */
    public FileStatus uploadFile(String localFilePath) throws IOException {
        return uploadFile(new File(localFilePath), this.ftpServerInfo.getRemotePath());
    }

    /**
     * 内部使用，文件上传
     *
     * @param file
     * @param remoteFilePath
     * @return
     * @throws IOException
     */
    private FileStatus uploadFile(File file, String remoteFilePath) throws IOException {
        //设置文件传输方式
        ftpServerInfo.getFtpClient().setFileType(FTP.BINARY_FILE_TYPE);
        //校验远程目录,不存在则创建
        if (buildRemoteDre(getDrePath(remoteFilePath), this.ftpServerInfo.getFtpClient()) == FileStatus.Create_Directory_Fail) {
            return FileStatus.Create_Directory_Fail;
        }
        //检查远程是否存在文件
        FTPFile[] ftpFiles = ftpServerInfo.getFtpClient().listFiles(new String(remoteFilePath.getBytes(this.ftpServerInfo.getCharSet()), "iso-8859-1"));
        //FTP服务器不存在此文件,执行上传新文件逻辑
        if (ftpFiles.length == 0) {
            return uploadNewFile(file, remoteFilePath);
        }
        FTPFile ftpFile = ftpFiles[0];
        //todo 存在文件，执行断点续传逻辑
        if (file.length() == ftpFile.getSize()) {
            //大小一致，不在上传
            return FileStatus.File_Exits;
        } else if (file.length() < ftpFile.getSize()) {
            //服务器文件大于本地文件 todo 是否需要删除重新上传
            return FileStatus.Remote_Bigger_Local;
        }
        return resumeUploadFromBreakPoint(file, remoteFilePath, ftpFile.getSize());
    }

    /**
     * ftp服务器某个目录下的文件下载至本地
     *
     * @param localDrectionPath 本地文件夹
     * @param recursive         是否递归文件夹
     * @return
     * @throws IOException
     */
    public FileStatus downDre(String localDrectionPath, boolean recursive) throws IOException, InvocationTargetException, IllegalAccessException {
        FileStatus fileStatus = judgeFtpPath(this.ftpServerInfo.getRemotePath());
        if (FileStatus.File_ok != fileStatus) {
            return fileStatus;
        }
        //获取FTP目录下所有文件，支持递归
        List<FTPFileInfo> reverseFtpFile = getReverseFtpFile(this.ftpServerInfo.getRemotePath(), recursive);
        if (CollectionUtils.isEmpty(reverseFtpFile)) {
            return FileStatus.Remote_File_Noexist;
        }
        //开始下载
        for (FTPFileInfo ftpFileInfo : reverseFtpFile) {
            if (StringUtils.isNotBlank(localDrectionPath)) {
                //本地目录入参存在，则执行存储本地逻辑
                downFile(localDrectionPath + seprator + ftpFileInfo.getName(), ftpFileInfo.getPath());
            }
            if (CollectionUtils.isNotEmpty(tarGetFtpServerInfoList)) {
                for (FtpServerInfo serverInfo : tarGetFtpServerInfoList) {
                    if (serverInfo.getFtpClient().isConnected()) {
                        downFileFromFtpToFtp(ftpFileInfo.getPath(), serverInfo.getRemotePath() + seprator + ftpFileInfo.getName(), serverInfo.getFtpClient());
                    }
                }
            }
        }
        return null;
    }

    /**
     * 文件下载
     *
     * @param localFilepath 文件路径
     * @return
     * @throws IOException
     */
    public FileStatus downFile(String localFilepath) throws IOException {
        return downFileFromFtpToFtp(localFilepath);
    }

    private FileStatus downFileFromFtpToFtp(String localFilePath) throws IOException {
        if (StringUtils.isNotBlank(localFilePath)) {
            //本地目录入参存在，则执行存储本地逻辑
            downFile(localFilePath, this.ftpServerInfo.getRemotePath()+ seprator + getFileNameByFilePath(localFilePath));
        }

        if (CollectionUtils.isNotEmpty(tarGetFtpServerInfoList)) {
            for (FtpServerInfo serverInfo : tarGetFtpServerInfoList) {
                if (serverInfo.getFtpClient().isConnected()) {
                    downFileFromFtpToFtp(ftpServerInfo.getRemotePath() + seprator + getFileNameByFilePath(localFilePath), serverInfo.getRemotePath() + seprator + getFileNameByFilePath(localFilePath), serverInfo.getFtpClient());
                }
            }
        }
        return null;
    }

    /**
     * FTP目录到FTP目录的传递
     *
     * @param fromFtpPath
     * @param toFtpPath
     * @param ftpClient   目标服务器client
     * @return
     * @throws IOException
     */
    private FileStatus downFileFromFtpToFtp(String fromFtpPath, String toFtpPath, FTPClient ftpClient) throws IOException {
        //构建ftp

        FileStatus filestatus = this.buildRemoteDre(getDrePath(toFtpPath), ftpClient);
        if (FileStatus.File_ok != filestatus) {
            return filestatus;
        }
        //检查远程是否存在文件
        //获取FTP目录下所有文件，支持递归
        FTPFile[] fromFtpFile = ftpServerInfo.getFtpClient().listFiles(new String(fromFtpPath.getBytes(this.ftpServerInfo.getCharSet()), "iso-8859-1"));
        //检查远程是否存在文件
        FTPFile[] toFtpFiles = ftpClient.listFiles(new String(toFtpPath.getBytes(this.ftpServerInfo.getCharSet()), "iso-8859-1"));

        //本地不存在，新文件下载
        if (toFtpFiles.length == 0) {
            //新文件
            return downNewFileFromFtpToFtp(fromFtpPath, toFtpPath, ftpClient);
        }
        if (fromFtpFile[0].getSize() == toFtpFiles[0].getSize()) {
            //大小一致，不在上传
            return FileStatus.File_Exits;
        } else if (fromFtpFile[0].getSize() < toFtpFiles[0].getSize()) {
            //大小一致，不在上传
            return FileStatus.Local_Bigger_Remote;
        }
        //断点下载
        return resumeDownFromFtpToFtpBreakPoint(fromFtpPath, toFtpPath, ftpClient, toFtpFiles[0].getSize());
    }
    private FileStatus downNewFileFromFtpToFtp(String fromFtpPath, String toFtpPath, FTPClient ftpClient) throws IOException {
        return resumeDownFromFtpToFtpBreakPoint(fromFtpPath, toFtpPath, ftpClient, 0);
    }

    private FileStatus resumeDownFromFtpToFtpBreakPoint(String fromFtpPath, String toFtpPath, FTPClient ftpClient, long size) throws IOException {
        ftpServerInfo.getFtpClient().setFileType(FTP.BINARY_FILE_TYPE);
        ftpClient.setFileType(FTP.BINARY_FILE_TYPE);
        this.ftpServerInfo.getFtpClient().setRestartOffset(size);
        InputStream inputStream = this.ftpServerInfo.getFtpClient().retrieveFileStream(fromFtpPath);
        ftpClient.setRestartOffset(size);
        OutputStream outputStream= ftpClient.appendFileStream(toFtpPath);
        byte [] bytes=new byte[1024];
        int c;
        while((c=inputStream.read(bytes))!=-1){
            outputStream.write(bytes,0,c);
        }
        inputStream.close();
        outputStream.close();
        return ftpServerInfo.getFtpClient().completePendingCommand() &&ftpClient.completePendingCommand()? FileStatus.Upload_From_Break_Success : FileStatus.Upload_From_Break_Failed;
        //return ftpClient.appendFile(toFtpPath, inputStream) ? FileStatus.File_ok : FileStatus.Download_New_Failed;
    }


    /**
     * 文件从FTP下载至本地
     *
     * @param localFilepath  本地文件路径
     * @param remoteFilePath FTP文件路径
     * @return
     * @throws IOException
     */
    private FileStatus downFile(String localFilepath, String remoteFilePath) throws IOException {
        FTPFile[] ftpFile = this.ftpServerInfo.getFtpClient().listFiles(remoteFilePath);
        if (ftpFile.length == 0) {
            return FileStatus.Remote_File_Noexist;
        }
        File file = new File(localFilepath);
        //目录不存在强制构建
        if (!file.exists()) {
            FileUtils.forceMkdir(file);
        }
        long localSize = 0l;
        //本地不存在，新文件下载
        if (!file.exists()) {
            return downNewFile(file, remoteFilePath, ftpFile[0].getSize());
        }
        if (localSize == ftpFile[0].getSize()) {
            //大小一致，不在上传
            return FileStatus.File_Exits;
        } else if (localSize > ftpFile[0].getSize()) {
            //大小一致，不在上传
            return FileStatus.Local_Bigger_Remote;
        }
        //断点下载
        return resumeDownFromBreakPoint(file, remoteFilePath, file.length(), ftpFile[0].getSize());
    }

    private FileStatus downNewFile(File file, String remoteFilePath, long size) throws IOException {
        return resumeDownFromBreakPoint(file, remoteFilePath, 0l, size);
    }

    /**
     * 断点下载
     *
     * @param file
     * @param remoteFilePath
     * @param localSize
     * @param remoteSize
     * @return
     * @throws IOException
     */
    private FileStatus resumeDownFromBreakPoint(File file, String remoteFilePath, long localSize, long remoteSize) throws IOException {
        //设置以二进制方式传输
        ftpServerInfo.getFtpClient().setFileType(FTP.BINARY_FILE_TYPE);
        ftpServerInfo.getFtpClient().setRestartOffset(localSize);
        InputStream in = ftpServerInfo.getFtpClient().retrieveFileStream(new String(remoteFilePath.getBytes("gbk"), "iso-8859-1"));
        FileOutputStream fileOutputStream = new FileOutputStream(file, true);
        byte[] bytes = new byte[1024];
        long step = remoteSize / 100;
        long process = localSize / step;
        int c;
        while ((c = in.read(bytes)) != -1) {
            fileOutputStream.write(bytes, 0, c);
            localSize += c;
            long nowProcess = localSize / step;
            if (nowProcess > process) {
                process = nowProcess;
                logger.info("下载进度：{}", process + "%");
            }
        }
        in.close();
        fileOutputStream.close();
        return ftpServerInfo.getFtpClient().completePendingCommand() ? FileStatus.Upload_From_Break_Success : FileStatus.Upload_From_Break_Failed;
    }

    /**
     * 校验FTP路径
     *
     * @param remoteDrectionPath
     * @return
     * @throws IOException
     */
    private FileStatus judgeFtpPath(String remoteDrectionPath) throws IOException {
        //路径为空
        if (StringUtils.isBlank(remoteDrectionPath)) {
            return FileStatus.Remote_File_Noexist;
        }
        //此路径在FTP服务器上不存在
        if (!ftpServerInfo.getFtpClient().changeWorkingDirectory(remoteDrectionPath)) {
            return FileStatus.Remote_Drection_notExists;
        }
        FTPFile[] ftpFiles = this.ftpServerInfo.getFtpClient().listFiles(remoteDrectionPath);
        //路径下在ftp服务器上不存在任何文件和文件夹
        if (ftpFiles.length == 0) {
            return FileStatus.Remote_File_Noexist;
        }
        return FileStatus.File_ok;
    }

    /**
     * 递归获取FTP远程目录下的所有文件
     *
     * @param remoteDrectionPath FTP远程目录
     * @param recursive          是否递归文件夹
     * @throws IOException
     */
    private List<FTPFileInfo> getReverseFtpFile(String remoteDrectionPath, boolean recursive) throws IOException, InvocationTargetException, IllegalAccessException {
        FTPFile[] ftpFiles = ftpServerInfo.getFtpClient().listFiles(new String(remoteDrectionPath.getBytes(ftpServerInfo.getFtpClient().getCharset()), "iso-8859-1"));
        List<FTPFileInfo> ftpFileList = new ArrayList<>();
        for (FTPFile ftpFile : ftpFiles) {
            if (ftpFile.isFile()) {
                FTPFileInfo ftpFileInfo = new FTPFileInfo();
                ftpFileInfo.setPath(remoteDrectionPath + seprator + ftpFile.getName());
                BeanUtils.copyProperties(ftpFileInfo, ftpFile);
                ftpFile = null; //释放占用空间
                ftpFileList.add(ftpFileInfo);
            } else if (ftpFile.isDirectory() && recursive) {
                ftpFileList.addAll(getReverseFtpFile(remoteDrectionPath + seprator + ftpFile.getName(), recursive));
            }
        }
        return ftpFileList;
    }

    /**
     * 上传新文件到FTP
     *
     * @param file
     * @param remoteFilePath
     * @return
     * @throws IOException
     */
    private FileStatus uploadNewFile(File file, String remoteFilePath) throws IOException {
        return resumeUploadFromBreakPoint(file, remoteFilePath, 0);
    }

    private void connect(FtpServerInfo ftpServerInfo) throws IOException {
        FTPClient ftpClient = ftpServerInfo.getFtpClient();
        //设置将过程中使用到的命令输出到控制台
        //ftpClient.addProtocolCommandListener(new PrintCommandListener(new PrintWriter(System.out)));
        ftpClient.connect(ftpServerInfo.getIp(), ftpServerInfo.getPort());
        //设置编码
        ftpClient.setControlEncoding(ftpServerInfo.getCharSet());
        //设置PassiveMode传输
        ftpClient.enterLocalPassiveMode();
        if (FTPReply.isPositiveCompletion(ftpClient.getReplyCode())) {
            if (ftpClient.login(ftpServerInfo.getUsername(), ftpServerInfo.getPassword())) {
                logger.info("FTP服务器{}连接成功", ftpServerInfo.getIp());
                return;
            }
        }
        disconnect();
        logger.info("FTP服务器{}连接失败", ftpServerInfo.getIp());
    }

    /**
     * 断点续传方式上传文件到FTP
     *
     * @param file
     * @param remoteFilePath
     * @param remoteSize
     * @return
     * @throws IOException
     */
    private FileStatus resumeUploadFromBreakPoint(File file, String remoteFilePath, long remoteSize) throws IOException {
        //具有指针，可指定文件读写指针位置
        RandomAccessFile rafile = new RandomAccessFile(file, "r");
        OutputStream out = ftpServerInfo.getFtpClient().appendFileStream(new String(remoteFilePath.getBytes(this.ftpServerInfo.getFtpClient().getCharset()), "iso-8859-1"));
        long step = file.length() / 100;
        long process = 0;
        long localreadbytes = 0L;
        //指定读取指针位置
        rafile.seek(remoteSize);
        //设置文件操作的起始位置
        ftpServerInfo.getFtpClient().setRestartOffset(remoteSize);
        process = remoteSize / step;
        localreadbytes = remoteSize;
        byte[] bytes = new byte[1024];
        int end;
        logger.info("上传文件:{}", remoteFilePath);
        while ((end = rafile.read(bytes)) != -1) {
            out.write(bytes, 0, end);
            localreadbytes += end;
            if (localreadbytes / step != process) {
                process = localreadbytes / step;
                logger.info("上传进度：{}", process + "%");
            }
        }
        out.flush();
        rafile.close();
        out.close();
        return ftpServerInfo.getFtpClient().completePendingCommand() ? FileStatus.Upload_From_Break_Success : FileStatus.Upload_From_Break_Failed;
    }

    /**
     * 本地文件夹路径校验
     *
     * @param localDrectionPath
     * @param recursive
     * @return
     */
    private FileStatus localPathJudge(String localDrectionPath, boolean recursive) {
        //本地路径是否存在
        if (StringUtils.isBlank(localDrectionPath)) {
            return FileStatus.Local_Drection_notExists;
        }
        File file = new File(localDrectionPath);
        //本地路径文件夹是否存在
        if (!file.exists()) {
            return FileStatus.Local_Drection_notExists;
        }
        //本地路径是否为文件路径
        if (!file.isDirectory()) {
            return FileStatus.Upload_Not_DrectionPath;
        }
        //本地路径下是否存在文件
        if (CollectionUtils.isEmpty(FileUtils.listFiles(file, null, recursive))) {
            return FileStatus.Upload_File_NotExists;
        }
        return FileStatus.File_ok;
    }

    /**
     * @param remotePath 远程ftp绝对路径
     */
    private FileStatus buildRemoteDre(String remotePath, FTPClient ftpClient) throws IOException {
        if (StringUtils.isBlank(remotePath)) {
            return FileStatus.Local_Drection_notExists;
        }
        //先校验完整路径是否存在
        if (ftpClient.changeWorkingDirectory(remotePath)) {
            return FileStatus.File_ok;
        }
        //过滤掉开头和结尾有/而导致的空数据
        List<String> nodeList = Arrays.asList(remotePath.split(seprator)).stream().filter(node -> StringUtils.isNotBlank(node)).collect(Collectors.toList());
        String nodePath = StringUtils.EMPTY;
        for (String node : nodeList) {
            nodePath += seprator + node;
            if (ftpClient.changeWorkingDirectory(nodePath)) {
                continue;
            }
            if (!ftpClient.makeDirectory(nodePath)) {
                return FileStatus.Create_Directory_Fail;
            }
        }
        ftpClient.changeToParentDirectory();
        System.out.println(ftpClient.printWorkingDirectory());
        return FileStatus.File_ok;
    }

    /**
     * 根据远程文件路径，返回对应的文件夹的路径
     *
     * @param filePath
     * @return
     */
    private String getDrePath(String filePath) {
        return filePath.substring(0, filePath.lastIndexOf("/") + 1);
    }

    private String getFileNameByFilePath(String name) {
        return name.substring(name.lastIndexOf("/") + 1, name.length());
    }

    /**
     * 断开与远程服务器的连接
     *
     * @throws IOException
     */
    public void disconnect() throws IOException {
        if (ftpServerInfo.getFtpClient().isConnected()) {
            ftpServerInfo.getFtpClient().disconnect();
        }
    }

    public static class Builder {
        //ftp服务器 ip地址
        private String ip;
        //ftp服务器 端口
        private int port;
        //ftp服务器用户名
        private String username;
        //ftp服务器密码
        private String password;
        //ftp服务器密码
        private String remotePath;
        //ftp 客户端字节编码
        private String charSet = CharEncoding.UTF_8;
        //ftp 传输方式  （二进制等）
        private int fileType = FTP.BINARY_FILE_TYPE;

        public Builder() {

        }

        public Builder setIp(String ip) {
            this.ip = ip;
            return this;
        }

        public Builder setPort(int port) {
            this.port = port;
            return this;
        }

        public Builder setUsername(String username) {
            this.username = username;
            return this;
        }

        public Builder setPassword(String password) {
            this.password = password;
            return this;
        }

        public Builder setCharSet(String charSet) {
            this.charSet = charSet;
            return this;
        }

        public Builder setFileType(int fileType) {
            this.fileType = fileType;
            return this;
        }

        public Builder setRemotePath(String remotePath) {
            this.remotePath = remotePath;
            return this;
        }

        public FtpTranClient1 build() {
            return new FtpTranClient1(this);
        }
    }
}